package by.gamedev.site.client.app.place;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.activity.shared.Activity;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceChangeEvent;
import com.google.gwt.place.shared.PlaceController;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.EntityProxyChange;
import com.google.gwt.requestfactory.shared.EntityProxyId;
import com.google.gwt.requestfactory.shared.Receiver;
import com.google.gwt.requestfactory.shared.Request;
import com.google.gwt.requestfactory.shared.WriteOperation;
import com.google.gwt.user.cellview.client.AbstractHasData;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.RangeChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SingleSelectionModel;

/**
 * Abstract activity for displaying a list of {@link EntityProxy}. These activities are not re-usable. Once they are stopped, they cannot be
 * restarted.
 * <p>
 * Subclasses must:
 * 
 * <ul>
 * <li>provide a {@link ProxyListView}
 * <li>implement method to request a full count
 * <li>implement method to find a range of entities
 * <li>respond to "show details" commands
 * </ul>
 * <p>
 * Only the properties required by the view will be requested.
 * 
 * @param <P>
 *            the type of {@link EntityProxy} listed
 */
public abstract class AbstractProxyListActivity<P extends EntityProxy> implements Activity, ProxyListView.Delegate<P> {

    /**
     * This mapping allows us to update individual rows as records change.
     */
    final Map<EntityProxyId<P>, Integer> idToRow   = new HashMap<EntityProxyId<P>, Integer>();
    final Map<EntityProxyId<P>, P>       idToProxy = new HashMap<EntityProxyId<P>, P>();

    private final PlaceController        placeController;
    final SingleSelectionModel<P>        selectionModel;
    private final Class<P>               proxyClass;

    private HandlerRegistration          rangeChangeHandler;
    ProxyListView<P>                     view;
    AcceptsOneWidget                     display;

    public AbstractProxyListActivity(final PlaceController placeController, final ProxyListView<P> view, final Class<P> proxyType) {
        this.view = view;
        this.placeController = placeController;
        this.proxyClass = proxyType;
        view.setDelegate(this);

        final HasData<P> hasData = view.asHasData();
        rangeChangeHandler = hasData.addRangeChangeHandler(new RangeChangeEvent.Handler() {
            @Override
            public void onRangeChange(final RangeChangeEvent event) {
                AbstractProxyListActivity.this.onRangeChanged(hasData);
            }
        });

        // Inherit the view's key provider
        final ProvidesKey<P> keyProvider = ((AbstractHasData<P>) hasData).getKeyProvider();
        selectionModel = new SingleSelectionModel<P>(keyProvider);
        hasData.setSelectionModel(selectionModel);

        selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
            @Override
            public void onSelectionChange(final SelectionChangeEvent event) {
                final P selectedObject = selectionModel.getSelectedObject();
                if(selectedObject != null ) {
                    showDetails(selectedObject);
                }
            }
        });
    }

    @Override
    public void createClicked() {
        placeController.goTo(new ProxyPlace(proxyClass));
    }

    public ProxyListView<P> getView() {
        return view;
    }

    @Override
    public String mayStop() {
        return null;
    }

    @Override
    public void onCancel() {
        onStop();
    }

    /**
     * Called by the table as it needs data.
     */
    public void onRangeChanged(final HasData<P> listView) {
        final Range range = listView.getVisibleRange();

        final Receiver<List<P>> callback = new Receiver<List<P>>() {
            @Override
            public void onSuccess(final List<P> values) {
                if(view == null ) {
                    // This activity is dead
                    return;
                }
                idToRow.clear();
                idToProxy.clear();
                for (int i = 0, row = range.getStart(); i < values.size(); i++, row++) {
                    final P proxy = values.get(i);
                    @SuppressWarnings("unchecked")
                    final// Why is this cast needed?
                    EntityProxyId<P> proxyId = (EntityProxyId<P>) proxy.stableId();
                    idToRow.put(proxyId, row);
                    idToProxy.put(proxyId, proxy);
                }
                getView().asHasData().setRowData(range.getStart(), values);
                if(display != null ) {
                    display.setWidget(getView());
                }
            }
        };

        fireRangeRequest(range, callback);
    }

    @Override
    public void onStop() {
        view.setDelegate(null);
        view = null;
        rangeChangeHandler.removeHandler();
        rangeChangeHandler = null;
    }

    /**
     * Select the given record, or clear the selection if called with null or an id we don't know.
     */
    public void select(final EntityProxyId<P> proxyId) {
        /*
         * The selectionModel will not flash if we put it back to the same state it is already in, so we can keep this code simple.
         */

        // Clear the selection
        final P selected = selectionModel.getSelectedObject();
        if(selected != null ) {
            selectionModel.setSelected(selected, false);
        }

        // Select the new proxy, if it's relevant
        if(proxyId != null ) {
            final P selectMe = idToProxy.get(proxyId);
            selectionModel.setSelected(selectMe, true);
        }
    }

    @Override
    public void start(final AcceptsOneWidget displayWidget, final EventBus eventBus) {
        view.setDelegate(this);
        EntityProxyChange.registerForProxyType(eventBus, proxyClass, new EntityProxyChange.Handler<P>() {
            @Override
            public void onProxyChange(final EntityProxyChange<P> event) {
                update(event.getWriteOperation(), event.getProxyId());
            }
        });
        eventBus.addHandler(PlaceChangeEvent.TYPE, new PlaceChangeEvent.Handler() {
            @Override
            public void onPlaceChange(final PlaceChangeEvent event) {
                updateSelection(event.getNewPlace());
            }
        });
        this.display = displayWidget;
        init();
        updateSelection(placeController.getWhere());
    }

    public void update(final WriteOperation writeOperation, final EntityProxyId<P> proxyId) {
        switch (writeOperation) {
            case UPDATE:
                update(proxyId);
                break;

            case DELETE:
                init();
                break;

            case PERSIST:
                /*
                 * On create, we presume the new record is at the end of the list, so fetch the last page of items.
                 */
                getLastPage();
                break;
        }
    }

    protected abstract Request<List<P>> createRangeRequest(Range range);

    protected abstract void fireCountRequest(Receiver<Long> callback);

    /**
     * Called when the user chooses a record to view. This default implementation sends the {@link PlaceController} to an appropriate
     * {@link ProxyPlace}.
     * 
     * @param record
     *            the chosen record
     */
    protected void showDetails(final P record) {
        placeController.goTo(new ProxyPlace(record.stableId(), ProxyPlace.Operation.DETAILS));
    }

    @SuppressWarnings("unchecked")
    private EntityProxyId<P> cast(final ProxyPlace proxyPlace) {
        return (EntityProxyId<P>) proxyPlace.getProxyId();
    }

    private void fireRangeRequest(final Range range, final Receiver<List<P>> callback) {
        createRangeRequest(range).with(getView().getPaths()).fire(callback);
    }

    private void getLastPage() {
        fireCountRequest(new Receiver<Long>() {
            @Override
            public void onSuccess(final Long response) {
                if(view == null ) {
                    // This activity is dead
                    return;
                }
                final HasData<P> table = getView().asHasData();
                final int rows = response.intValue();
                table.setRowCount(rows, true);
                if(rows > 0 ) {
                    final int pageSize = table.getVisibleRange().getLength();
                    final int remnant = rows % pageSize;
                    if(remnant == 0 ) {
                        table.setVisibleRange(rows - pageSize, pageSize);
                    } else {
                        table.setVisibleRange(rows - remnant, pageSize);
                    }
                }
                onRangeChanged(table);
            }
        });
    }

    private void init() {
        fireCountRequest(new Receiver<Long>() {
            @Override
            public void onSuccess(final Long response) {
                if(view == null ) {
                    // This activity is dead
                    return;
                }
                getView().asHasData().setRowCount(response.intValue(), true);
                onRangeChanged(view.asHasData());
            }
        });
    }

    private void update(final EntityProxyId<P> proxyId) {
        final Integer row = idToRow.get(proxyId);
        if(row == null ) {
            return;
        }
        fireRangeRequest(new Range(row, 1), new Receiver<List<P>>() {
            @Override
            public void onSuccess(final List<P> response) {
                getView().asHasData().setRowData(row, Collections.singletonList(response.get(0)));
            }
        });
    }

    void updateSelection(final Place newPlace) {
        if(newPlace instanceof ProxyPlace ) {
            final ProxyPlace proxyPlace = (ProxyPlace) newPlace;
            if(proxyPlace.getOperation() != ProxyPlace.Operation.CREATE && proxyPlace.getProxyClass().equals(proxyClass) ) {
                select(cast(proxyPlace));
                return;
            }
        }

        select(null);
    }
}
